﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.CompilerServices;

namespace DynamicProperties
{
	/// <summary>
	/// This class allows to use dynamic properties.
	/// </summary>
	public class Extender
	{
		#region Global static instance
		// single instance of global extender
		private static Extender global = null;
		private static object syncLock = new object();

		/// <summary>
		/// Gets global instance <see cref="Extender"/>.
		/// </summary>
		public static Extender Global 
		{
			get
			{
				// double check for lazy init
				if(global == null)
					lock(syncLock)
						if(global == null)
							global = new Extender();

				return global;
			}
		}

		#endregion

		private DynamicPropertyContainerStorage storage;

		/// <summary>
		/// Creates new instance of <see cref="Extender"/>.
		/// </summary>
		public Extender()
		{
			this.storage = new DynamicPropertyContainerStorage();
		}

		/// <summary>
		/// Sets dynamic property of given object to specific value.
		/// </summary>
		/// <typeparam name="TTarget">Type of target object to work with.</typeparam>
		/// <typeparam name="TPropType">Type of property value.</typeparam>
		/// <param name="target">Target object.</param>
		/// <param name="property">Property to set.</param>
		/// <param name="value">New value of dynamic property.</param>
		public void SetProperty<TTarget, TPropType>(TTarget target, DynamicProperty<TTarget, TPropType> property, TPropType value)
			where TTarget : class
		{
			// validate
			if (property == null)
				throw new ArgumentNullException("property");

			if (target == null)
				throw new ArgumentNullException("target");

			// resolve container
			var container = this.storage.GetOrCreateContainer(target);
			container.SetProperty(target, property, value);
		}

		/// <summary>
		/// Gets value of dynamic property of given object.
		/// </summary>
		/// <typeparam name="TTarget">Type of target object to work with.</typeparam>
		/// <typeparam name="TPropType">Type of property value.</typeparam>
		/// <param name="target">Target object.</param>
		/// <param name="property">Property to read.</param>
		/// <param name="result">Value of dynamic property.</param>
		/// <returns>True if dynamic property was found. False is returned if value is null or not defined yet.</returns>
		public bool TryGetProperty<TTarget, TPropType>(TTarget target, DynamicProperty<TTarget, TPropType> property, out TPropType result)
			where TTarget : class
		{ 
			// validate
			if (property == null)
				throw new ArgumentNullException("property");

			if (target == null)
				throw new ArgumentNullException("target");

			object resultBoxed;

			// resolve container
			var container = this.storage.GetContainer(target);

			if (container == null || !container.TryGetProperty(target, property, out resultBoxed))
			{
				// not found
				result = default(TPropType);
				return false;
			}
			else
			{
				// found
				result = (TPropType)resultBoxed;
				return true;
			}
		}

		/// <summary>
		/// Gets value of dynamic property of given object if exists.
		/// </summary>
		/// <typeparam name="TTarget">Type of target object to work with.</typeparam>
		/// <typeparam name="TPropType">Type of property value.</typeparam>
		/// <param name="target">Target object.</param>
		/// <param name="property">Property to read.</param>
		/// <returns>Value of dynamic property.</returns>
		public TPropType GetProperty<TTarget, TPropType>(TTarget target, DynamicProperty<TTarget, TPropType> property)
			where TTarget : class
		{
			// validate
			if (property == null)
				throw new ArgumentNullException("property");

			if (target == null)
				throw new ArgumentNullException("target");

			// resolve container
			var container = this.storage.GetOrCreateContainer(target);

			// get value (if not exists, default value is used and stored)
			object resultBoxed = container.GetProperty(target, property);
			
			// cast
			TPropType result = (TPropType)resultBoxed;
			return result;
		}

		/// <summary>
		/// Removes value of dynamic property of given object.
		/// </summary>
		/// <typeparam name="TTarget">Type of target object to work with.</typeparam>
		/// <typeparam name="TPropType">Type of property value.</typeparam>
		/// <param name="target">Target object.</param>
		/// <param name="property">Property to reset.</param>
		/// <returns>Returns true if property was found and reseted on given object.</returns>
		public bool ResetProperty<TTarget, TPropType>(TTarget target, DynamicProperty<TTarget, TPropType> property)
			where TTarget : class
		{
			// validate
			if (property == null)
				throw new ArgumentNullException("property");

			if (target == null)
				throw new ArgumentNullException("target");

			// resolve container
			var container = this.storage.GetContainer(target);

			// container not found so no properties was set yet for current instance
			if (container == null)
				return false;

			// removes property from container
			bool result = container.RemoveProperty(target, property);
			return result;
		}

		/// <summary>
		/// Removes all dynamic properties values from object.
		/// </summary>
		/// <param name="target">Target object.</param>
		/// <returns>Returns true if link to given object was found and removed. Returns false if no dynamic property link to given object was found.</returns>
		public bool ResetObject(object target)
		{
			// validate
			if (target == null)
				throw new ArgumentNullException("target");

			// remove container
			bool result = this.storage.RemoveContainer(target);
			return result;
		}

		public void RegisterOnChanged<TTarget, TPropType>(TTarget target, DynamicProperty<TTarget, TPropType> property, DynamicPropertyChangedCallback<TTarget, TPropType> callBack)
			where TTarget : class
		{
			// validate
			if (target == null)
				throw new ArgumentNullException("target");

			if (property == null)
				throw new ArgumentNullException("property");

			if (callBack == null)
				throw new ArgumentNullException("callBack");

			var eventSubscribers = EventSubscribers.EventSubscribersProperty.Get(target);
			eventSubscribers.RegisterSubscriber<TTarget, TPropType>(property, callBack);
		}

		public void UnregisterOnChanged<TTarget, TPropType>(TTarget target, DynamicProperty<TTarget, TPropType> property, DynamicPropertyChangedCallback<TTarget, TPropType> callBack)
			where TTarget : class
		{
			// validate
			if (target == null)
				throw new ArgumentNullException("target");

			if (property == null)
				throw new ArgumentNullException("property");

			if (callBack == null)
				throw new ArgumentNullException("callBack");

			var eventSubscribers = EventSubscribers.EventSubscribersProperty.Get(target);
			eventSubscribers.UnregisterSubscriber<TTarget, TPropType>(property, callBack);
		}

		/// <summary>
		/// Returns values of all dynamic properties of given object.
		/// </summary>
		/// <param name="target">Target object.</param>
		/// <returns>Array of assigned dynamic properties or null if no properties was found.</returns>
		public DynamicPropertyDump[] DumpObject(object target)
		{
			// validate
			if (target == null)
				throw new ArgumentNullException("target");

			// get container for reference
			var container = this.storage.GetContainer(target);

			// no container linked yet
			if (container == null)
				return DynamicPropertyDump.EmptyDumpArray;

			// fetch all dynamic properties values
			DynamicPropertyDump[] result;
			result = container.DumpAll();

			return result;
		}
	}
}
